/*
MIT License

Copyright(c) 2020 Evgeny Pereguda

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files(the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions :

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "Player.h"

PCWSTR szTitle = L"BasicPlayback";
PCWSTR szWindowClass = L"MFBASICPLAYBACK";

#define TIMERID_REFRESH		1

#define FREQUENCY_REFRESH		30

HINSTANCE   g_hInstance;                        // current instance
BOOL        g_bRepaintClient = TRUE;            // Repaint the application client area?
CPlayer     *g_pPlayer = NULL;                  // Global player object. 

// Note: After WM_CREATE is processed, g_pPlayer remains valid until the
// window is destroyed.

// Forward declarations of functions included in this code module:
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    OpenUrlDialogProc(HWND, UINT, WPARAM, LPARAM);
void                NotifyError(HWND hwnd, const WCHAR *sErrorMessage, HRESULT hr);
void                UpdateUI(HWND hwnd, PlayerState state);
HRESULT             AllocGetWindowText(HWND hwnd, WCHAR **pszText, DWORD *pcchLen);

// Message handlers
LRESULT             OnCreateWindow(HWND hwnd);
void                OnFileOpen(HWND hwnd);
void                OnOpenURL(HWND hwnd);
void                OnPlayerEvent(HWND hwnd, WPARAM pUnkPtr);
void                OnPaint(HWND hwnd);
void                OnResize(WORD width, WORD height);
void                OnKeyPress(WPARAM key);

// OpenUrlDialogInfo: Contains data passed to the "Open URL" dialog proc.
struct OpenUrlDialogInfo
{
	WCHAR *pszURL;
	DWORD cch;
};

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow)
{
	HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

	MSG msg;

	ZeroMemory(&msg, sizeof(msg));

	// Perform application initialization.
	if (!InitInstance(hInstance, nCmdShow))
	{
		NotifyError(NULL, L"Could not initialize the application.",
			HRESULT_FROM_WIN32(GetLastError()));
		return FALSE;
	}

	// Main message loop.
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	// Clean up.
	if (g_pPlayer)
	{
		g_pPlayer->Shutdown();
		SafeRelease(&g_pPlayer);
	}
	return 0;
}

//  Create the application window.
BOOL InitInstance(HINSTANCE hInst, int nCmdShow)
{
	HWND hwnd;
	WNDCLASSEX wcex;

	g_hInstance = hInst; // Store the instance handle.

	// Register the window class.
	ZeroMemory(&wcex, sizeof(WNDCLASSEX));
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.hInstance = hInst;
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = MAKEINTRESOURCE(IDC_MFPLAYBACK);
	wcex.lpszClassName = szWindowClass;

	if (RegisterClassEx(&wcex) == 0)
	{
		return FALSE;
	}

	// Create the application window.
	hwnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL);

	if (hwnd == 0)
	{
		return FALSE;
	}


	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);

	return TRUE;
}

VOID WINAPI CheckRendererItem(HWND hwnd, UINT uID)
{
	HMENU hmenuBar = GetMenu(hwnd);

	// Check the specified item and uncheck all the others. 

	CheckMenuRadioItem(
		hmenuBar,         // handle to menu 
		ID_WINDOW_HANDLER,           // first item in range 
		ID_DX9_SWAPCHAIN,          // last item in range 
		uID,                // item to check 
		MF_BYCOMMAND        // IDs, not positions 
		);

	g_pPlayer->setRenderType(uID);
}

VOID WINAPI CheckLibraryItem(HWND hwnd, UINT uID)
{
	HMENU hmenuBar = GetMenu(hwnd);

	// Check the specified item and uncheck all the others. 

	CheckMenuRadioItem(
		hmenuBar,         // handle to menu 
		ID_LIB_CM,           // first item in range 
		ID_LIB_VR,          // last item in range 
		uID,                // item to check 
		MF_BYCOMMAND        // IDs, not positions 
		);

	g_pPlayer->setLibraryType(uID);
}

//  Message handler for the main window.

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_CREATE:
		return OnCreateWindow(hwnd);

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDM_EXIT:
			DestroyWindow(hwnd);
			break;
		case ID_FILE_OPENFILE:
			OnFileOpen(hwnd);
			break;
		case ID_FILE_OPENURL:
			OnOpenURL(hwnd);
			break;
		default:

			if ((LOWORD(wParam)&0x0F00) == 0x0200)
			{
				CheckRendererItem(hwnd, LOWORD(wParam));

				break;
			}
			else if ((LOWORD(wParam) & 0x0F00) == 0x0300)
			{
				CheckLibraryItem(hwnd, LOWORD(wParam));

				break;
			}

			return DefWindowProc(hwnd, message, wParam, lParam);
		}
		break;

	case WM_PAINT:
		OnPaint(hwnd);
		break;

	case WM_SIZE:
		OnResize(LOWORD(lParam), HIWORD(lParam));
		break;

	case WM_ERASEBKGND:
		// Suppress window erasing, to reduce flickering while the video is playing.
		return 1;

	case WM_DESTROY:
		PostQuitMessage(0);
		break;

	case WM_CHAR:
		OnKeyPress(wParam);
		break;

	case WM_APP_PLAYER_EVENT:
		OnPlayerEvent(hwnd, wParam);
		break;

	default:
		return DefWindowProc(hwnd, message, wParam, lParam);
	}
	return 0;
}

//  Open an audio/video file.
void OnFileOpen(HWND hwnd)
{
	IFileOpenDialog *pFileOpen = NULL;
	IShellItem *pItem = NULL;
	PWSTR pszFilePath = NULL;

	// Create the FileOpenDialog object.
	HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
		CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
	if (FAILED(hr))
	{
		goto done;
	}

	// Show the Open dialog box.
	hr = pFileOpen->Show(NULL);
	if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))
	{
		// The user canceled the dialog. Do not treat as an error.
		hr = S_OK;
		goto done;
	}
	else if (FAILED(hr))
	{
		goto done;
	}

	// Get the file name from the dialog box.
	hr = pFileOpen->GetResult(&pItem);
	if (FAILED(hr))
	{
		goto done;
	}

	hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
	if (FAILED(hr))
	{
		goto done;
	}

	// Display the file name to the user.
	hr = g_pPlayer->OpenURL(pszFilePath);
	if (SUCCEEDED(hr))
	{
		UpdateUI(hwnd, OpenPending);
	}

done:
	if (FAILED(hr))
	{
		NotifyError(hwnd, L"Could not open the file.", hr);
		UpdateUI(hwnd, Closed);
	}
	CoTaskMemFree(pszFilePath);
	SafeRelease(&pItem);
	SafeRelease(&pFileOpen);
}

//  Open a media file from a URL.
void OnOpenURL(HWND hwnd)
{
	HRESULT hr = S_OK;

	// Pass in an OpenUrlDialogInfo structure to the dialog. The dialog 
	// fills in this structure with the URL. The dialog proc allocates
	// the memory for the string. 

	OpenUrlDialogInfo url;
	ZeroMemory(&url, sizeof(&url));

	// Show the Open URL dialog.
	if (IDOK == DialogBoxParam(g_hInstance, MAKEINTRESOURCE(IDD_OPENURL), hwnd,
		OpenUrlDialogProc, (LPARAM)&url))
	{
		// Open the file with the playback object.
		hr = g_pPlayer->OpenURL(url.pszURL);
		if (SUCCEEDED(hr))
		{
			UpdateUI(hwnd, OpenPending);
		}
		else
		{
			NotifyError(hwnd, L"Could not open this URL.", hr);
			UpdateUI(hwnd, Closed);
		}
	}

	// The caller must free the URL string.
	CoTaskMemFree(url.pszURL);
}

//  Handler for WM_CREATE message.
LRESULT OnCreateWindow(HWND hwnd)
{
	// Initialize the player object.
	HRESULT hr = CPlayer::CreateInstance(hwnd, hwnd, &g_pPlayer);
	if (SUCCEEDED(hr))
	{

		CheckRendererItem(hwnd, ID_WINDOW_HANDLER);

		CheckLibraryItem(hwnd, ID_LIB_CM);

		UpdateUI(hwnd, Closed);
		return 0;   // Success.
	}
	else
	{
		NotifyError(NULL, L"Could not initialize the player object.", hr);
		return -1;  // Destroy the window
	}
}

//  Handler for WM_PAINT messages.
void OnPaint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc = BeginPaint(hwnd, &ps);

	if (g_pPlayer && g_pPlayer->HasVideo())
	{
		// Video is playing. Ask the player to repaint.
		g_pPlayer->Repaint();
	}
	else
	{
		// The video is not playing, so we must paint the application window.
		RECT rc;
		GetClientRect(hwnd, &rc);
		FillRect(hdc, &rc, (HBRUSH)COLOR_WINDOW);
	}
	EndPaint(hwnd, &ps);
}

//  Handler for WM_SIZE messages.
void OnResize(WORD width, WORD height)
{
	if (g_pPlayer)
	{
		g_pPlayer->ResizeVideo(width, height);
	}
}


// Handler for WM_CHAR messages. 
void OnKeyPress(WPARAM key)
{
	switch (key)
	{
		// Space key toggles between running and paused
	case VK_SPACE:
		if (g_pPlayer->GetState() == Started)
		{
			g_pPlayer->Pause();
		}
		else if (g_pPlayer->GetState() == Paused)
		{
			g_pPlayer->Play();
		}
		break;
	}
}

// Handler for Media Session events.
void OnPlayerEvent(HWND hwnd, WPARAM pUnkPtr)
{
	HRESULT hr = g_pPlayer->HandleEvent(pUnkPtr);
	if (FAILED(hr))
	{
		NotifyError(hwnd, L"An error occurred.", hr);
	}
	UpdateUI(hwnd, g_pPlayer->GetState());
}


void CALLBACK TimerIDRefresh(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
	if (hwnd != NULL)
	{
		g_pPlayer->Refresh();
	}
}

// Update the application UI to reflect the current state.

void UpdateUI(HWND hwnd, PlayerState state)
{
	BOOL bWaiting = FALSE;
	BOOL bPlayback = FALSE;

	assert(g_pPlayer != NULL);

	switch (state)
	{
	case OpenPending:
		bWaiting = TRUE;
		break;

	case Started:
		bPlayback = TRUE;
		::KillTimer(hwnd,  TIMERID_REFRESH);
		::SetTimer(hwnd, TIMERID_REFRESH, FREQUENCY_REFRESH, TimerIDRefresh);
		break;

	case Paused:
		bPlayback = TRUE;
		break;
	}

	HMENU hMenu = GetMenu(hwnd);
	UINT  uEnable = MF_BYCOMMAND | (bWaiting ? MF_GRAYED : MF_ENABLED);

	EnableMenuItem(hMenu, ID_FILE_OPENFILE, uEnable);
	EnableMenuItem(hMenu, ID_FILE_OPENURL, uEnable);

	if (bPlayback && g_pPlayer->HasVideo())
	{
		g_bRepaintClient = FALSE;
	}
	else
	{
		g_bRepaintClient = TRUE;
	}
}

//  Show a message box with an error message.
void NotifyError(HWND hwnd, PCWSTR pszErrorMessage, HRESULT hrErr)
{
	const size_t MESSAGE_LEN = 512;
	WCHAR message[MESSAGE_LEN];

	if (SUCCEEDED(StringCchPrintf(message, MESSAGE_LEN, L"%s (HRESULT = 0x%X)",
		pszErrorMessage, hrErr)))
	{
		MessageBox(hwnd, message, NULL, MB_OK | MB_ICONERROR);
	}
}


//  Dialog proc for the "Open URL" dialog.
INT_PTR CALLBACK OpenUrlDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	static OpenUrlDialogInfo *pUrl = NULL;

	BOOL result = FALSE;

	switch (message)
	{
	case WM_INITDIALOG:
		// The caller sends a pointer to an OpenUrlDialogInfo structure as the 
		// lParam. This structure stores the URL.
		pUrl = (OpenUrlDialogInfo*)lParam;
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		switch (LOWORD(wParam))
		{
		case IDOK:
			if (pUrl)
			{
				// Get the URL from the edit box in the dialog. This function 
				// allocates memory. The caller must call CoTaskMemAlloc.
				if (SUCCEEDED(AllocGetWindowText(GetDlgItem(hDlg, IDC_EDIT_URL),
					&pUrl->pszURL, &pUrl->cch)))
				{
					result = TRUE;
				}
			}
			EndDialog(hDlg, result ? IDOK : IDABORT);
			break;

		case IDCANCEL:
			EndDialog(hDlg, LOWORD(IDCANCEL));
			break;
		}
		return (INT_PTR)FALSE;
	}
	return (INT_PTR)FALSE;
}

// Helper function to get text from a window.
//
// This function allocates a buffer and returns it in pszText. The caller must
// call CoTaskMemFree on the buffer.
//
// hwnd:     Handle to the window.
// pszText:  Receives a pointer to the string.
// pcchLen:  Receives the length of the string, in characters, not including
//           the terminating NULL character. 

HRESULT AllocGetWindowText(HWND hwnd, WCHAR **pszText, DWORD *pcchLen)
{
	if (pszText == NULL || pcchLen == NULL)
	{
		return E_POINTER;
	}

	*pszText = NULL;

	int cch = GetWindowTextLength(hwnd);
	if (cch < 0)
	{
		return E_UNEXPECTED;
	}

	PWSTR pszTmp = (PWSTR)CoTaskMemAlloc(sizeof(WCHAR) * (cch + 1));
	// Includes room for terminating NULL character

	if (!pszTmp)
	{
		return E_OUTOFMEMORY;
	}

	if (cch == 0)
	{
		pszTmp[0] = L'\0';  // No text.
	}
	else
	{
		int res = GetWindowText(hwnd, pszTmp, (cch + 1));
		// Size includes terminating null character.

		// GetWindowText returns 0 if (a) there is no text or (b) it failed.
		// We checked for (a) already, so 0 means failure here.
		if (res == 0)
		{
			CoTaskMemFree(pszTmp);
			return __HRESULT_FROM_WIN32(GetLastError());
		}
	}

	// If we got here, szTmp is valid, so return it to the caller.
	*pszText = pszTmp;

	// Return the length NOT including the '\0'.
	*pcchLen = static_cast<DWORD>(cch);

	return S_OK;
}
